﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace GridField
{
	public partial class GridFieldControl : UserControl
	{
		#region Fields

		/// <summary>
		/// Верхний левый видимый угол решетки
		/// </summary>
		private Point _topLeftPoint = new Point(0, 0);

		/// <summary>
		/// Размеры решетки
		/// </summary>
		private Size _gridSize;

		/// <summary>
		/// Фон в виде сетки
		/// </summary>
		private Bitmap _backGroundGridImage;

		/// <summary>
		/// Сами клетки
		/// </summary>
		private Bitmap _foreGroundMainImage;

		/// <summary>
		/// Список с клетками, необходимыми для отображения
		/// </summary>
		private IList<Point> _currentCells;

		#endregion


		#region Constructors

		public GridFieldControl(Size gridSize)
		{
			InitializeComponent();

			CreateGridBackground();

			this._gridSize = gridSize;

			_currentCells = new List<Point>(10);
		}

		public GridFieldControl(int width = 10, int heigth = 10) : this(new Size(width, heigth)) { }

		public GridFieldControl() : this(40, 40) { }

		#endregion


		#region Methods

		#region private methods

		private void CreateGridBackground()
		{
			this._backGroundGridImage = new Bitmap(11, 11);
			var data = this._backGroundGridImage.LockBits(
				new Rectangle(0, 0, this._backGroundGridImage.Width, this._backGroundGridImage.Height), ImageLockMode.ReadWrite,
				PixelFormat.Format24bppRgb);

			int bytes = data.Stride * this._backGroundGridImage.Height;
			var rgbValues = new byte[bytes];
			for (int i = 0; i < data.Stride; i++) rgbValues[i] = 64;
			for (int i = data.Stride; i < bytes - 2; i += data.Stride)
			{
				rgbValues[i] = 64;
				rgbValues[i + 1] = 64;
				rgbValues[i + 2] = 64;
			}
			System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, data.Scan0, bytes);
			this._backGroundGridImage.UnlockBits(data);
			this.pictureField.BackgroundImage = this._backGroundGridImage;
			this.pictureField.BackgroundImageLayout = ImageLayout.Tile;
		}

		/// <summary>
		/// Обновляет картинку фореграунда поля с клетками
		/// </summary>
		private void UpdateField()
		{
			// Текущий размер решетки;
			int pictureWidth = this.pictureField.Width;
			int pictureHeight = this.pictureField.Height;

			//Видимая ширина и высота решетки в координатах решетки (количество видимых клеток в строке и в столбце)
			int visibleGridWidth = pictureWidth / 11;
			int visibleGridHeght = pictureHeight / 11;

			// Создаем новый битмап
			this._foreGroundMainImage = new Bitmap(pictureWidth, pictureHeight, PixelFormat.Format32bppArgb);
			var bmpData = _foreGroundMainImage.LockBits(new Rectangle(0, 0, pictureWidth, pictureHeight), ImageLockMode.ReadWrite,
														PixelFormat.Format32bppArgb);

			var bytes = bmpData.Stride * pictureHeight;
			var argbData = new byte[bytes];

			//Перерисовываем полные клетки
			this.RedrawFullCells(visibleGridWidth, visibleGridHeght, argbData, bmpData.Stride);

			//TODO: Добавить отрисовку неполных клеток

			System.Runtime.InteropServices.Marshal.Copy(argbData, 0, bmpData.Scan0, bytes);
			_foreGroundMainImage.UnlockBits(bmpData);

			this.pictureField.Image = _foreGroundMainImage;
		}

		/// <summary>
		/// Метод рисует полные клетки в видимой области
		/// </summary>
		/// <param name="visibleGridWidth">Количество клеток в видимой области по оси X</param>
		/// <param name="visibleGridHeight">Количество клеток в видимой области по оси Y</param>
		/// <param name="argbData">Массив байт с данными картинки</param>
		/// <param name="stride">Ширина строки картинки в байтах</param>
		private void RedrawFullCells(int visibleGridWidth, int visibleGridHeight, byte[] argbData, int stride)
		{
			//Вычисляем сколько всего элементов находится в правой нижней четверти плоскости после начала видимой области
			int numXAfterLeftUpperCorner = (_gridSize.Width - _topLeftPoint.X);
			int numYAfterLeftUpperCorner = (_gridSize.Height - _topLeftPoint.Y);

			// Количество клеток, которые возможно отобразить по каждой из осей
			int numXCellsForUpdate = Math.Min(visibleGridWidth, numXAfterLeftUpperCorner);
			int numYCellsForUpdate = Math.Min(visibleGridHeight, numYAfterLeftUpperCorner);

			//Выбираем все клетки, попавшие в видимую область.
			var visibleCells = from data in _currentCells
							   where
								data.X >= _topLeftPoint.X & 
								data.X < _topLeftPoint.X + numXCellsForUpdate &
								data.Y >= _topLeftPoint.Y & 
								data.Y < _topLeftPoint.Y + numYCellsForUpdate
							   select data;


			foreach (Point cell in visibleCells)
			{
				//Вычисляем абсолютные координаты левого верхнего угла клетки
				int xAbsolute = (cell.X - _topLeftPoint.X) * 11 + 1;
				int yAbsolute = (cell.Y - _topLeftPoint.Y) * 11 + 1;
				PaintCell(new Rectangle(xAbsolute, yAbsolute, 10, 10),Color.ForestGreen,argbData, stride );
			}
		}

		/// <summary>
		/// Метод для закрашивания квадратной области 10х10 пикселей с заданным координатами ВЛУ. Цвет ARGB = [255; 0; 128; 64]
		/// </summary>
		/// <param name="x">координата X верхнего левого угла области</param>
		/// <param name="y">координата Y верхнего левого угла области</param>
		/// <param name="argbValues">массив данных</param>
		/// <param name="stride">длина графической строки в байтах</param>
		/// <exception cref="ArgumentOutOfRangeException">Возникает в случае если длина массива данных
		/// меньше чем необходимо для закрашивания области</exception>
		private static void PaintCell(int x, int y, IList<byte> argbValues, int stride)
		{
			PaintCell(new Rectangle(x, y, 10, 10), Color.FromArgb(255, 0, 128, 64), argbValues, stride);
		}

		/// <summary>
		/// Метод для закрашивания прямоугольной области.
		/// </summary>
		/// <param name="rectangle">Область</param>
		/// <param name="color">Цвет</param>
		/// <param name="argbValues">массив данных битмапа в котором нада закрасить область</param>
		/// <param name="stride">длина графической строки в байтах</param>
		/// <exception cref="ArgumentOutOfRangeException">Возникает в случае если длина массива данных
		/// меньше чем необходимо для закрашивания области</exception>
		private static void PaintCell(Rectangle rectangle, Color color, IList<byte> argbValues, int stride)
		{
			if ((rectangle.Y + rectangle.Height) * stride + (rectangle.X + rectangle.Width) * 4 + 4 > argbValues.Count) throw new ArgumentOutOfRangeException();
			for (int numY = 0; numY < rectangle.Height; numY++)
			{
				for (int numX = 0; numX < rectangle.Width; numX++)
				{
					int currentPosition = (rectangle.Y + numY) * stride + (rectangle.X + numX) * 4;
					argbValues[currentPosition] = color.B;			//Blue
					argbValues[currentPosition + 1] = color.G;		//Green
					argbValues[currentPosition + 2] = color.R;		//Red
					argbValues[currentPosition + 3] = color.A;		//Alpha
				}
			}
		}

		#endregion


		#region EventRaise Methods

		protected override void OnPaint(PaintEventArgs e)
		{
			this.UpdateField();

			base.OnPaint(e);
		}

		protected virtual void OnDataChanged(EventArgs e)
		{
			UpdateField();

			EventHandler<EventArgs> handler = Interlocked.CompareExchange(ref this.DataChanged, null, null);
			if (handler != null) handler(this, e);
		}

		protected virtual void OnGridSizeChanged(EventArgs e)
		{
			EventHandler<EventArgs> handler = Interlocked.CompareExchange(ref this.GridSizeChanged, null, null);
			if (handler != null) handler(this, e);
		}

		#endregion

		#endregion


		#region Properties

		public IList<Point> Data
		{
			get { return _currentCells; }
			set
			{
				if (value.Any(obj => obj.X >= _gridSize.Width |
										obj.Y >= _gridSize.Height |
										obj.X < 0 |
										obj.Y < 0)) throw new ArgumentOutOfRangeException("value");
				_currentCells = value;
				OnDataChanged(new EventArgs());
			}
		}

		public Size GridSize
		{
			get { return this._gridSize; }
			set
			{
				if (value.Height <= 0 | value.Width <= 0) throw new ArgumentOutOfRangeException();
				this._gridSize = value;
				this.OnGridSizeChanged(new EventArgs());
				this.Invalidate();
			}
		}

		#endregion


		#region Events

		public event EventHandler<EventArgs> GridSizeChanged;

		public event EventHandler<EventArgs> DataChanged;

		#endregion
	}
}
